// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fs-management/mount.h>

#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#include <fbl/algorithm.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_call.h>
#include <fbl/unique_fd.h>
#include <fbl/unique_ptr.h>
#include <fbl/vector.h>
#include <fs/client.h>
#include <fuchsia/hardware/block/c/fidl.h>
#include <fuchsia/io/c/fidl.h>
#include <lib/fdio/limits.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/vfs.h>
#include <lib/fzl/fdio.h>
#include <lib/zx/channel.h>
#include <pretty/hexdump.h>
#include <zircon/compiler.h>
#include <zircon/device/block.h>
#include <zircon/device/vfs.h>
#include <zircon/processargs.h>
#include <zircon/syscalls.h>

#include <utility>

namespace {

using fbl::unique_fd;

zx_status_t MountFs(int fd, zx_handle_t root) {
  zx_status_t status;
  fzl::FdioCaller caller{fbl::unique_fd(fd)};
  zx_status_t io_status = fuchsia_io_DirectoryAdminMount(caller.borrow_channel(), root, &status);
  caller.release().release();
  if (io_status != ZX_OK) {
    return io_status;
  }
  return status;
}

void UnmountHandle(zx_handle_t root, bool wait_until_ready) {
  // We've entered a failure case where the filesystem process (which may or may not be alive)
  // had a *chance* to be spawned, but cannot be attached to a vnode (for whatever reason).
  // Rather than abandoning the filesystem process (maybe causing dirty bits to be set), give it a
  // chance to shutdown properly.
  //
  // The unmount process is a little atypical, since we're just sending a signal over a handle,
  // rather than detaching the mounted filesystem from the "parent" filesystem.
  vfs_unmount_handle(root, wait_until_ready ? ZX_TIME_INFINITE : 0);
}

// Performs the actual work of mounting a volume.
class Mounter {
 public:
  // The mount point is either a path (to be created) or an existing fd.
  explicit Mounter(int fd) : path_(nullptr), fd_(fd) {}
  explicit Mounter(const char* path) : path_(path), fd_(-1) {}
  ~Mounter() {}

  // Mounts the given device.
  zx_status_t Mount(zx::channel device, disk_format_t format, const mount_options_t& options,
                    LaunchCallback cb);

  DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Mounter);

 private:
  zx_status_t PrepareHandles(zx::channel device);
  zx_status_t MakeDirAndMount(const mount_options_t& options);
  zx_status_t LaunchAndMount(LaunchCallback cb, const mount_options_t& options, const char** argv,
                             int argc);
  zx_status_t MountNativeFs(const char* binary, zx::channel device, const mount_options_t& options,
                            LaunchCallback cb);
  zx_status_t MountFat(zx::channel device, const mount_options_t& options, LaunchCallback cb);

  zx_handle_t root_ = ZX_HANDLE_INVALID;
  const char* path_;
  int fd_;
  uint32_t flags_ = 0;  // Currently not used.
  size_t num_handles_ = 0;
  zx_handle_t handles_[2];
  uint32_t ids_[2];
};

// Initializes 'handles_' and 'ids_' with the root handle and block device handle.
zx_status_t Mounter::PrepareHandles(zx::channel block_device) {
  zx::channel root_server, root_client;
  zx_status_t status = zx::channel::create(0, &root_server, &root_client);
  if (status != ZX_OK) {
    return status;
  }
  handles_[0] = root_server.release();
  ids_[0] = FS_HANDLE_ROOT_ID;
  handles_[1] = block_device.release();
  ids_[1] = FS_HANDLE_BLOCK_DEVICE_ID;
  num_handles_ = 2;

  root_ = root_client.release();
  return ZX_OK;
}

zx_status_t Mounter::MakeDirAndMount(const mount_options_t& options) {
  auto cleanup =
      fbl::MakeAutoCall([this, options]() { UnmountHandle(root_, options.wait_until_ready); });

  // Open the parent path as O_ADMIN, and sent the mkdir+mount command
  // to that directory.
  char parent_path[PATH_MAX];
  const char* name;
  strcpy(parent_path, path_);
  char* last_slash = strrchr(parent_path, '/');
  if (last_slash == NULL) {
    strcpy(parent_path, ".");
    name = path_;
  } else {
    *last_slash = '\0';
    name = last_slash + 1;
    if (*name == '\0') {
      return ZX_ERR_INVALID_ARGS;
    }
  }

  unique_fd parent(open(parent_path, O_RDONLY | O_DIRECTORY | O_ADMIN));
  if (!parent) {
    return ZX_ERR_IO;
  }

  cleanup.cancel();

  zx_status_t status;
  fzl::FdioCaller caller(std::move(parent));
  zx_status_t io_status = fuchsia_io_DirectoryAdminMountAndCreate(
      caller.borrow_channel(), root_, name, strlen(name), flags_, &status);
  if (io_status != ZX_OK) {
    return io_status;
  }
  return status;
}

// Calls the 'launch callback' and mounts the remote handle to the target vnode, if successful.
zx_status_t Mounter::LaunchAndMount(LaunchCallback cb, const mount_options_t& options,
                                    const char** argv, int argc) {
  auto cleanup =
      fbl::MakeAutoCall([this, options]() { UnmountHandle(root_, options.wait_until_ready); });

  zx_status_t status = cb(argc, argv, handles_, ids_, num_handles_);
  if (status != ZX_OK) {
    return status;
  }

  if (options.wait_until_ready) {
    // Wait until the filesystem is ready to take incoming requests
    zx_signals_t observed;
    status = zx_object_wait_one(root_, ZX_USER_SIGNAL_0 | ZX_CHANNEL_PEER_CLOSED, ZX_TIME_INFINITE,
                                &observed);
    if ((status != ZX_OK) || (observed & ZX_CHANNEL_PEER_CLOSED)) {
      status = (status != ZX_OK) ? status : ZX_ERR_BAD_STATE;
      return status;
    }
  }
  cleanup.cancel();

  // Install remote handle.
  if (options.create_mountpoint) {
    return MakeDirAndMount(options);
  }
  return MountFs(fd_, root_);
}

zx_status_t Mounter::MountNativeFs(const char* binary, zx::channel device,
                                   const mount_options_t& options, LaunchCallback cb) {
  zx_status_t status = PrepareHandles(std::move(device));
  if (status != ZX_OK) {
    return status;
  }

  if (options.verbose_mount) {
    printf("fs_mount: Launching %s\n", binary);
  }

  // 1. binary
  // 2. (optional) readonly
  // 3. (optional) verbose
  // 4. (optional) metrics
  // 5. command
  fbl::Vector<const char*> argv;
  argv.push_back(binary);
  if (options.readonly) {
    argv.push_back("--readonly");
  }
  if (options.verbose_mount) {
    argv.push_back("--verbose");
  }
  if (options.collect_metrics) {
    argv.push_back("--metrics");
  }
  if (options.enable_journal) {
    argv.push_back("--journal");
  }
  argv.push_back("mount");
  argv.push_back(nullptr);
  return LaunchAndMount(cb, options, argv.data(), static_cast<int>(argv.size() - 1));
}

zx_status_t Mounter::MountFat(zx::channel device, const mount_options_t& options,
                              LaunchCallback cb) {
  zx_status_t status = PrepareHandles(std::move(device));
  if (status != ZX_OK) {
    return status;
  }

  if (options.verbose_mount) {
    printf("fs_mount: FAT not presently supported\n");
  }

  return ZX_ERR_NOT_SUPPORTED;
}

zx_status_t Mounter::Mount(zx::channel device, disk_format_t format, const mount_options_t& options,
                           LaunchCallback cb) {
  switch (format) {
    case DISK_FORMAT_MINFS:
      return MountNativeFs("/boot/bin/minfs", std::move(device), options, cb);
    case DISK_FORMAT_BLOBFS:
      return MountNativeFs("/boot/bin/blobfs", std::move(device), options, cb);
    case DISK_FORMAT_FAT:
      return MountFat(std::move(device), options, cb);
    default:
      return ZX_ERR_NOT_SUPPORTED;
  }
}

}  // namespace

const mount_options_t default_mount_options = {
    .readonly = false,
    .verbose_mount = false,
    .collect_metrics = false,
    .wait_until_ready = true,
    .create_mountpoint = false,
    .enable_journal = true,
};

const mkfs_options_t default_mkfs_options = {
    .fvm_data_slices = 1,
    .verbose = false,
};

const fsck_options_t default_fsck_options = {
    .verbose = false,
    .never_modify = false,
    .always_modify = false,
    .force = false,
    .apply_journal = false,
};

enum DiskFormatLogVerbosity {
  Silent,
  Verbose,
};

disk_format_t detect_disk_format_impl(int fd, DiskFormatLogVerbosity verbosity) {
  if (lseek(fd, 0, SEEK_SET) != 0) {
    fprintf(stderr, "detect_disk_format: Cannot seek to start of device.\n");
    return DISK_FORMAT_UNKNOWN;
  }

  fuchsia_hardware_block_BlockInfo info;
  fzl::UnownedFdioCaller caller(fd);
  zx_status_t status;
  zx_status_t io_status =
      fuchsia_hardware_block_BlockGetInfo(caller.borrow_channel(), &status, &info);
  if (io_status != ZX_OK || status != ZX_OK) {
    fprintf(stderr, "detect_disk_format: Could not acquire block device info\n");
    return DISK_FORMAT_UNKNOWN;
  }

  // We expect to read HEADER_SIZE bytes, but we may need to read
  // extra to read a multiple of the underlying block size.
  const size_t buffer_size =
      fbl::round_up(static_cast<size_t>(HEADER_SIZE), static_cast<size_t>(info.block_size));

  uint8_t data[buffer_size];
  if (read(fd, data, buffer_size) != static_cast<ssize_t>(buffer_size)) {
    fprintf(stderr, "detect_disk_format: Error reading block device.\n");
    return DISK_FORMAT_UNKNOWN;
  }

  if (!memcmp(data, fvm_magic, sizeof(fvm_magic))) {
    return DISK_FORMAT_FVM;
  }

  if (!memcmp(data, zxcrypt_magic, sizeof(zxcrypt_magic))) {
    return DISK_FORMAT_ZXCRYPT;
  }

  if (!memcmp(data + 0x200, gpt_magic, sizeof(gpt_magic))) {
    return DISK_FORMAT_GPT;
  }

  if (!memcmp(data, minfs_magic, sizeof(minfs_magic))) {
    return DISK_FORMAT_MINFS;
  }

  if (!memcmp(data, blobfs_magic, sizeof(blobfs_magic))) {
    return DISK_FORMAT_BLOBFS;
  }

  if ((data[510] == 0x55 && data[511] == 0xAA)) {
    if ((data[38] == 0x29 || data[66] == 0x29)) {
      // 0x55AA are always placed at offset 510 and 511 for FAT filesystems.
      // 0x29 is the Boot Signature, but it is placed at either offset 38 or
      // 66 (depending on FAT type).
      return DISK_FORMAT_FAT;
    }
    return DISK_FORMAT_MBR;
  }

  if (verbosity == DiskFormatLogVerbosity::Verbose) {
    // Log a hexdump of the bytes we looked at and didn't find any magic in.
    fprintf(stderr, "detect_disk_format: did not recognize format.  Looked at:\n");
    // fvm, zxcrypt, minfs, and blobfs have their magic bytes at the start
    // of the block.
    hexdump_very_ex(data, 16, 0, hexdump_stdio_printf, stderr);
    // MBR is two bytes at offset 0x1fe, but print 16 just for consistency
    hexdump_very_ex(data + 0x1f0, 16, 0x1f0, hexdump_stdio_printf, stderr);
    // GPT magic is stored 512 bytes in, so it can coexist with MBR.
    hexdump_very_ex(data + 0x200, 16, 0x200, hexdump_stdio_printf, stderr);
  }

  return DISK_FORMAT_UNKNOWN;
}

disk_format_t detect_disk_format(int fd) {
  return detect_disk_format_impl(fd, DiskFormatLogVerbosity::Silent);
}

disk_format_t detect_disk_format_log_unknown(int fd) {
  return detect_disk_format_impl(fd, DiskFormatLogVerbosity::Verbose);
}

zx_status_t fmount(int device_fd, int mount_fd, disk_format_t df, const mount_options_t* options,
                   LaunchCallback cb) {
  Mounter mounter(mount_fd);

  zx::channel block_device;
  zx_status_t status = fdio_get_service_handle(device_fd, block_device.reset_and_get_address());
  if (status != ZX_OK) {
    return status;
  }

  return mounter.Mount(std::move(block_device), df, *options, cb);
}

zx_status_t mount(int device_fd, const char* mount_path, disk_format_t df,
                  const mount_options_t* options, LaunchCallback cb) {
  if (!options->create_mountpoint) {
    // Open mountpoint; use it directly.
    unique_fd mount_point(open(mount_path, O_RDONLY | O_DIRECTORY | O_ADMIN));
    if (!mount_point) {
      return ZX_ERR_BAD_STATE;
    }
    return fmount(device_fd, mount_point.get(), df, options, cb);
  }

  Mounter mounter(mount_path);

  zx::channel block_device;
  zx_status_t status = fdio_get_service_handle(device_fd, block_device.reset_and_get_address());
  if (status != ZX_OK) {
    return status;
  }
  return mounter.Mount(std::move(block_device), df, *options, cb);
}

zx_status_t fumount(int mount_fd) {
  zx_handle_t h;
  zx_status_t status;
  fzl::FdioCaller caller{fbl::unique_fd(mount_fd)};
  zx_status_t io_status =
      fuchsia_io_DirectoryAdminUnmountNode(caller.borrow_channel(), &status, &h);
  caller.release().release();
  if (io_status != ZX_OK) {
    return io_status;
  }
  zx::channel c(h);
  if (status != ZX_OK) {
    return status;
  }
  return vfs_unmount_handle(c.release(), ZX_TIME_INFINITE);
}

zx_status_t umount(const char* mount_path) {
  fprintf(stderr, "Unmounting %s\n", mount_path);
  unique_fd fd(open(mount_path, O_DIRECTORY | O_NOREMOTE | O_ADMIN));
  if (!fd) {
    fprintf(stderr, "Could not open directory: %s\n", strerror(errno));
    return ZX_ERR_BAD_STATE;
  }
  zx_status_t status = fumount(fd.get());
  return status;
}
